Desarrollado por Diego Lesmes
¿Qué datos crees que te ayudarían a trabajar en el problema? ¿Por qué?
Dentro de los diferentes atributos o características que podrían ser útiles para determinar el valor de una propiedad, se pueden tener en cuenta características como:
En general esta información podría ser muy util para poder realizar un modelo predictivo que determine el valor de la propiedad, debido a que estas características inciden principalmente a la hora de realizar la venta de las propiedades, sin embargo el modelo esta en función del procedimiento que se realice en la información disponible y el análisis estadístico pertinente.
from google.colab import files data_to_load = files.upload()
pip install folium
import io
import pandas as pd
import seaborn as sns
sns.set_style('darkgrid')
import pandas.util.testing as tm
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
import statsmodels.api as sm
import statsmodels.formula.api as smf
from sklearn import tree
from sklearn.tree import export_graphviz
from sklearn.tree import DecisionTreeClassifier
from sklearn import metrics
from sklearn import ensemble
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_curve, auc, accuracy_score
from sklearn.model_selection import StratifiedKFold, train_test_split
from sklearn import neighbors
import folium #needed for interactive map
from folium.plugins import HeatMap
import wordcloud as wordcloud
from wordcloud import WordCloud
def RMSE(prediction,true_values): # Root Mean Squared Error
return np.sqrt(np.mean(np.square(prediction-true_values)))
def MAE(prediction,true_values): #Mean Absolute Error
return np.mean(np.abs(prediction-true_values))
def MAPE(prediction,true_value): #Mean Absolute Error Percenage
return np.mean(np.abs((prediction-true_value)/true_value)*100)
df = pd.read_csv('DS_Proyecto_01_Datos_Properati.csv')
df.head()
df = pd.read_csv(io.BytesIO(data_to_load['DS_Proyecto_01_Datos_Properati.csv'])) df.head()
df.shape
df.columns
features = pd.DataFrame(df.dtypes)
features.columns = ['type']
features_f = features[features['type'] == 'float64'].index
features_o = features[features['type'] == 'object'].index
features.sort_values('type')
Observación: Las variables que debería ser tipo fecha estan siendo leidas como texto
df.isnull().sum()
df.describe(include='object')
Observación: Las variables l1, currency y operation_type solo tienen una categoría, por lo que no son útiles en el estudio. Las variables de fecha parecen ser un registro por cada día del año
df['property_type'].value_counts()
unique = df['l3'].unique()
Observación: Solo tres categorías de la variable "Property type" tienen una cantidad importante de registros
text_full_clean =' '.join(df['l3'])
cloud = WordCloud(background_color="green",stopwords = None).generate(text_full_clean.lower())
plt.figure(figsize = (8, 8), facecolor = None)
plt.imshow(cloud)
plt.axis("off")
plt.tight_layout(pad = 0)
Observación: El wordCloud nos muestra los barrios con mayor número de propiedades registradas en la plataforma
round(df.describe(include='float64'),2)
Observación: Se evidencia la presencia de outliers y la dimensión de las variables, lo que sugiere la necesidad de algun tratamiento inicial a la data
df['l1'].unique()
Observación: Hay varios registros cuya ubicación no corresponde a Argentina
plt.rcParams.update({'font.size': 25})
var_hist = features_f
plt.figure(figsize=(25,6*len(features_f)/2))
for i,var in enumerate(var_hist):
plt.subplot(len(features_f)/2,2,i+1)
sns.distplot(df[var],fit=stats.norm, kde=False)
plt.xlabel(var)
plt.ylabel("count")
Observación: Se evidencia la presencia de outliers y el orden de magnitud de la variables. Lo que evita ver la distribución normal de las mismas
clean = np.where(
(df["lat"] < -21) & # norte
(df["lat"] > -67) & # sur
(df["lon"] > -73) & # este
(df["lon"] < -53) & # oeste
(df["price"] > 0) &
(df["surface_total"] > 0) &
(df["surface_covered"] > 0) &
#(df["surface_total"] > df["surface_covered"]) &
#(pd.to_numeric(df["end_date"]) < pd.to_numeric(pd.Timestamp.now())) &
(df["bathrooms"] > 0) #&
)
df_cleaned = df.iloc[clean].copy()
df_cleaned['start_date_P'] = pd.DatetimeIndex(df_cleaned["start_date"]).month #pd.to_datetime(df_cleaned["start_date"]).dt.to_period("M")
#df_cleaned['end_date_P'] = pd.to_datetime(pd.Timestamp.date(pd.to_datetime(df_cleaned["end_date"]))).dt.to_period("M")
df_cleaned['created_on_P'] = pd.DatetimeIndex(df_cleaned["start_date"]).month #pd.to_datetime(df_cleaned["created_on"]).dt.to_period("M")
#df_cleaned = df_cleaned[df_cleaned.property_type.isin(["Departamento", "Casa", "PH"])]
Observación: Se aplican las observaciones identificadas previamente, para realizar la validación inicial de los datos.
features_numeric = ['rooms', 'bathrooms', 'surface_total', 'surface_covered', 'price']
BC_matrix = []
for var in features_numeric:
BC_matrix.append(stats.boxcox(abs(df_cleaned[var])))
BC_matrix = pd.DataFrame(BC_matrix)
BC_matrix.index = features_numeric
BC_matrix.columns = ['Parametros', 'Lambda']
round(BC_matrix['Lambda'].head(5),2)
Observación: Los valores de lambda negativos para una función BoxCox, recomiendan realizar una transformación logaritmica de las variables numéricas.
df_cleaned['log_Price'] = np.log(df_cleaned['price'])
df_cleaned['log_Price_per_S_total'] = np.log(df_cleaned['price']/df_cleaned["surface_total"])
df_cleaned['log_Price_per_S_covered'] = np.log(df_cleaned['price']/df_cleaned["surface_covered"])
df_cleaned['log_S_total'] = np.log(df_cleaned["surface_total"])
df_cleaned['log_S_covered'] = np.log(df_cleaned["surface_covered"])
Observación: Se realiza la transformación de las variables mas relevantes. Adicionalmente se crean intuitivamente las variables log_Prices_per_S_covered y log_Prices_per_S_total, para analizar los precios por magnitud de área, posteriormente se validara la utilidad de la misma
df_cleaned.shape
'Observación: Después de aplicar la validación, se observa una reducción del {0}% de la cantidad de información'.format(round(len(df_cleaned)/len(df)-1,4)*100)
features = pd.DataFrame(df_cleaned.dtypes)
features.columns = ['type']
features_f = features[features['type'] == 'float64'].index
features_o = features[features['type'] == 'object'].index
features.shape
#features
round(df_cleaned.describe(include='float64'),2)
Observación: La transformación y validación inicial hacen sentido, pues promedio y la desviación estandar se ajusta con los cuantiles de cada variable nueva
round(df_cleaned.describe(include='int64'))
#df_cleaned.describe(include='object')
Observación: Se analiza las variables tipo entero, que son resultado de extraer el mes de las variables de fecha, dado que solo contemplan un periodo de 13 meses, si esta variable comprendiera un periodo mayor si seria interesante ver la variación en un intervalo mayor
var_hist = ['lat', 'lon', 'rooms', 'bedrooms', 'bathrooms',
'log_Price', 'log_Price_per_S_total', 'log_Price_per_S_covered', 'log_S_total', 'log_S_covered','start_date_P','created_on_P']
len(var_hist)
plt.rcParams.update({'font.size': 20})
plt.figure(figsize=(25,6*5))
for i,var in enumerate(var_hist):
plt.subplot(6,2,i+1)
sns.distplot(df_cleaned[var],fit=stats.norm, kde=False, color='g')
plt.xlabel(var)
plt.ylabel("count")
Observación: Se puede observar que la mayoría de las variables se distribuyen normal, solo tres variables tienen una disribución con cola derecha, la cual podría ser corregida, de ser necesario.
df_cleaned.isnull().sum()
Observación: Ya no se tienen registros con valores nulos o faltantes
covariables = ['start_date_P','created_on_P','l2', 'l3', 'property_type','rooms', 'bedrooms', 'bathrooms',
'lat','lon','log_Price_per_S_total','log_Price_per_S_covered','log_S_total','log_S_covered','price','log_Price']
len(covariables)
plt.rcParams.update({'font.size': 20})
hight = 10
wide = 20
col = 2
row = 8
plt.figure(figsize=(wide*col,hight*row))
for i,var in enumerate(covariables):
plt.subplot(row,col,i+1)
if var in ['start_date_P','created_on_P','l2', 'l3','bathrooms','rooms','bedrooms','property_type']:
sns.violinplot(x=var, y='log_Price', data=df_cleaned)
plt.xticks(rotation=90)
else:
sns.scatterplot(x=var,y='log_Price',data=df_cleaned, alpha=0.10,color='r')
Observación: A través de las gráficas de violines, se puede ver la distribuición de las variables categoricas con respecto a la variable dependiente, si bien no se percibe alguna tendencia en ellas, en particular la variable rooms si muestra los outliers que tiene. Por su parte los gráficos de dispersión permiten ver la distribución de las variables numéricas con respecto a la variable independiente, sin embargo no muestra un claro indicio de heterocedasticidad o tendencia. Las últimas dos gráficas permiten visualizar la transformación realizada a la variable dependiente.
plt.rcParams.update({'font.size': 10})
#compute correlation matrix
df_correlations = df_cleaned.corr()
#mask the upper half for visualization purposes
mask = np.zeros_like(df_correlations, dtype=np.bool)
mask[np.triu_indices_from(mask)] = True
# Draw the heatmap with the mask and correct aspect ratio
plt.figure(figsize= (10,10))
sns.heatmap(df_correlations, mask=mask, cmap="GnBu_r",#"RdYlBu",
annot=True, square=True,
#vmin=-0.9, vmax=0.9,
fmt="+.1f")
plt.title("Correlations between predictors")
Observación: El analisis de correlaciones nos permite identifficar que variables numericas pueden representar una posible multicolinealidad en los modelos a realizar.
Las correlaciones mas fuertes se evidencian entre:
covariables
covariables = ['start_date_P',
#'created_on_P',
'l2',
'l3',
'property_type',
'rooms',
'bedrooms',
'bathrooms',
'lat',
'lon',
'log_Price_per_S_total',
#'log_Price_per_S_covered',
#'log_S_total',
'log_S_covered'
#'price',
#'log_Price'
]
print(len(covariables))
formula = ' + '.join(covariables)
'log_Price~'+formula
Observación: Se descartan las covariables que presentan una fuerte correlación fruto de relaciones inmediatas que presentan con otras variables.
plt.rcParams.update({'font.size': 15})
df_temp = df_cleaned[covariables]
#compute correlation matrix
df_correlations = df_temp.corr()
#mask the upper half for visualization purposes
mask = np.zeros_like(df_correlations, dtype=np.bool)
mask[np.triu_indices_from(mask)] = True
# Draw the heatmap with the mask and correct aspect ratio
plt.figure(figsize= (10,10))
sns.heatmap(df_correlations, mask=mask, cmap="GnBu_r",#"RdYlBu",
annot=True, square=True,
#vmin=-0.9, vmax=0.9,
fmt="+.1f")
plt.title("Correlations between predictors")
Observación: Aun existen correlaciones altas entre Lon vs Lat y Bedrooms vs roooms, se verificara su p-value en el analisis estadistico a realizar
np.random.seed(1)
ndata = len(df_cleaned)
# Randomly choose 0.8n indices between 1 and n
idx_train = np.random.choice(range(ndata),int(0.7*ndata),replace=False)
# The test set is comprised from all the indices that were
# not selected in the training set:
idx_test = np.asarray(list(set(range(ndata)) - set(idx_train)))
train = df_cleaned.iloc[idx_train] # the training data set
test = df_cleaned.iloc[idx_test] # the test data set
print(train.shape)
print(test.shape)
Observación Se realiza la partición de la información 70/30 para probar el modelo creando asi los conjuntos de prueba y entrenamiento
df_cleaned.columns
model_rl= smf.ols(formula = "log_Price~start_date_P + l2 + l3 + property_type + rooms + bedrooms + bathrooms + lat + lon + log_Price_per_S_total + log_S_covered", data = train).fit()
print(model_rl.summary())
resultados1 = ('modelo: rl R²: {}, AIC: {}, RMSE: {}, MAE: {} y MAPE: {}%'.format(
round(model_rl.rsquared,2),
round(model_rl.aic,2),
round(RMSE(np.exp(model_rl.predict(test)),test.price),2),
round(MAE(np.exp(model_rl.predict(test)),test.price),2),
round(MAPE(np.exp(model_rl.predict(test)),test.price),2))
)
print(resultados1)
Observación: El modelo inicial de regresión lineal contempla las variables numericas y categoricas definidas en el paso anterior. Sin embargo, a pesar de que el modelo presenta un R² alto del 85% y un MAPE del 19.05%, vemos que el AIC y el número condicional tambien son considerablemente altos lo que implica la presencia de multicolinealidad en las covariables.
p = model_rl.pvalues
print(p[p<=0.05/11].tail(15))
print(p[p<=0.05/11].head(5))
Observación: Los p-Value mayores a 5% se presentan en diferentes barrios, asi mismo la variable _propertyType presenta un p-value alto, por lo menos para la categoría Depósito; indicando insuficiencia estadistica en estas variables, lo que sugiere descartarla para reducir su multicolienalidad, sin emnbargo esto no ocurre para tadas las categorias, por lo cual se matienen esta variables dentro del estudio.
df2 = df_cleaned.copy()
df2['l3'].value_counts()
df2 = df_cleaned.copy()
df2['property_type'].value_counts()
Observación: La variable de barrios precisamente indica que tiene categorías con muy pocos registros, por su parte, la covariable _propertyType solo cuenta con tres categorías con un cantidad significativa de registros, lo que muestra un claro desbalance en la información.
Es por esto que se puede realizar un segundo modelo que se enfoque en una categoría que tenga datos suficientes para no presentar un desbalance significativo.
property_type = ['Departamento']#, 'PH']#, 'Casa', 'Casa de campo', 'Oficina']
df2 = df_cleaned.copy()
for i in property_type:
df2 = df2[df2['property_type'] == 'Departamento']
#Particion
np.random.seed(1)
ndata = len(df2)
# Randomly choose 0.8n indices between 1 and n
idx_train = np.random.choice(range(ndata),int(0.8*ndata),replace=False)
# The test set is comprised from all the indices that were
# not selected in the training set:
idx_test = np.asarray(list(set(range(ndata)) - set(idx_train)))
train2 = df2.iloc[idx_train] # the training data set
test2 = df2.iloc[idx_test] # the test data set
#RunModel
model_rl2 = smf.ols(formula = "log_Price~start_date_P + l2 + l3 + property_type + rooms + bedrooms + bathrooms + lat + lon + log_Price_per_S_total+ log_S_covered", data = train2).fit()
#Parametros
print(model_rl2.summary())
resultados2 = ('modelo: rl2 {}, R²: {}, AIC: {}, RMSE: {}, MAE: {} y MAPE: {}%'.format(i,
round(model_rl2.rsquared,2),
round(model_rl2.aic,2),
round(RMSE(np.exp(model_rl2.predict(test2)),test2.price),2),
round(MAE(np.exp(model_rl2.predict(test2)),test2.price),2),
round(MAPE(np.exp(model_rl2.predict(test2)),test2.price),2))
)
print(resultados2)
Observación: El segundo modelo de regresión lineal se realiza con las mismas variables que el primero,con excepción de que se realizó específicamente para el tipo de propiedad "Departamento", de acuerdo con lo indentificado en el paso anterior. Sin embargo, a pesar de que el modelo presenta un R² alto del 92% y un AIC menor que cero, vemos que el MAPE se redujo 7 puntos porcetuales.
print(resultados1)
print(resultados2)
Observación: Con el segundo modelo de regresión lineal podemos realizar las predicciones del precio de un nuevo departamento, mientras que con el primero podemos realizar la predicción del precio de cualquier tipo de propiedad, solo que con un mayor margen de error, por ello es pertinente analizar los residuales de ambos modelos.
#plt.figure(figsize=(7,5))
sns.distplot(model_rl.resid,fit=stats.norm, kde=False, label='modelo_inicial')
sns.distplot(model_rl2.resid,fit=stats.norm, kde=False, label='modelo_Departamentos')
plt.legend()
plt.ylabel("count")
plt.title('Residuals')
Observación: Los residuales del "modelo_departamentos" presentan menor desviación estandar y mayor concentración en la media.
plt.figure(figsize=(15,10))
sns.scatterplot(x='lon', y='lat', data=df_cleaned, hue="l2",
size="price")
plt.title("Ciudad de Buenos Aires",fontsize=20)
Observación: Visualizamos geográficamente como esta distribuida la información del estudio, por cada una de las zonas de Buenos Aires.
df_cleaned['Area_price'] = 'Medio'
min_index = df_cleaned[df_cleaned['log_Price_per_S_total'] < np.percentile(df_cleaned['log_Price_per_S_total'], 25)].index
max_index = df_cleaned[df_cleaned['log_Price_per_S_total'] > np.percentile(df_cleaned['log_Price_per_S_total'], 75)].index
df_cleaned.loc[min_index,'Area_price'] ='Bajo'
df_cleaned.loc[max_index,'Area_price'] ='Alto'
df_cleaned['Area_price'].unique()
Observación: Se crea la variable _Areprice para identificar las priedades de mayor valor, aquellas superiores al 75% del precio por área de las propiedades regisradas en la plataforma
plt.figure(figsize=(15,10))
sns.scatterplot(x='lon', y='lat', data=df_cleaned, hue="Area_price",
size="surface_total")
plt.title("Valor de las propiedades",fontsize=20)
Observación: Se puede observa cómo se distribuye geográficamente el valor de las diferentes propiedades, en el centro de la ciudad se aprecian las mas costosas, mientras que en la zona periferica se observan las propiedades de menor valuación.
Propiedades de mayor costo por Área
df_CA = df_cleaned[df_cleaned['Area_price']=='Alto']
max_amount = float(df_CA['price'].max())
folium_hmap = folium.Map(location=[-34.6, -58.4],
zoom_start=13,
tiles="OpenStreetMap")
hm_wide = HeatMap( list(zip(df_CA['lat'], df_CA['lon'], df_CA['price'])),
min_opacity=0.2,
max_val=max_amount,
radius=8, blur=6,
max_zoom=15,
)
folium_hmap.add_child(hm_wide)
df_CA['l3'].value_counts()
Observación: Los barrios con mayor cantidad de propiedades de precio por área mas costoso son Palermo, Belgrano, Recoleta, Tigre y puerto madero.
df_CA.describe(include='object').loc[:,('l2','l3','property_type')]
df_CA['property_type'].value_counts()
Observación: Las propiedades de área mas costosa, se localizan en las 4 zonas de Bienos Aires, en 74 barrios y se trata de 8 tipos propiedades diferentes, excluyendo las casas de campo y las cocheras
round(df_CA.describe(include='float64').loc[:,('rooms','bedrooms','bathrooms','surface_total','surface_covered')])
Observación: El percentil 75% de las propiedades de área mas costosa, tienen menos de 4 salones, 3 habitaciones, 2 baños, una superficie total de 110 unidades y una superfice cubierta de 100 unidades, por su parte el cuarto cuantil de las propiedades de area mas costosas tienen entre 5 y 14 salones, entre 4 y 8 habitaciones, entre 3 y 14 baños, entre 111 y 1150 unidades de area total y entre 101 y 13402 unidades de area cubierta.
A continuación se realiza graficamente la descripción de estas características, por tipo de propiedad y zona a la que pertenecen.
plt.rcParams.update({'font.size': 15})
plt.figure(figsize=(20,8))
sns.boxplot(x='property_type', y='surface_total', data=df_CA, hue='l2')
plt.xticks(rotation=45)
plt.title("Tipo de propiedad vs. Area Total", fontsize=20, verticalalignment='bottom')
plt.rcParams.update({'font.size': 15})
plt.figure(figsize=(20,8))
sns.boxplot(x='property_type', y='log_S_covered', data=df_CA, hue='l2')
plt.xticks(rotation=45)
plt.title("Tipo de propiedad vs. Area Costruida", fontsize=20, verticalalignment='bottom')
plt.rcParams.update({'font.size': 15})
plt.figure(figsize=(20,8))
sns.boxplot(x='property_type', y='rooms', data=df_CA, hue='l2')
plt.xticks(rotation=45)
plt.title("Tipo de propiedad vs. Salones", fontsize=20, verticalalignment='bottom')
plt.rcParams.update({'font.size': 15})
plt.figure(figsize=(20,8))
sns.boxplot(x='property_type', y='bedrooms', data=df_CA, hue='l2')
plt.xticks(rotation=90)
plt.title("Tipo de propiedad vs. habitaciones", fontsize=20, verticalalignment='bottom')
plt.rcParams.update({'font.size': 15})
plt.figure(figsize=(20,8))
sns.boxplot(x='property_type', y='bathrooms', data=df_CA, hue='l2')
plt.xticks(rotation=90)
plt.title("Tipo de propiedad vs. Baños", fontsize=20, verticalalignment='bottom')
plt.rcParams.update({'font.size': 15})
plt.figure(figsize=(20,8))
sns.boxplot(x='property_type', y='log_Price_per_S_total', data=df_CA, hue='l2')
plt.xticks(rotation=90)
plt.title("Tipo de propiedad vs. Precio", fontsize=20, verticalalignment='bottom')
La Arquitectura propuesta se puede apreciar en git: https://dlesmes.github.io/Antivirus/4_Diagram_Architecture.pdf